<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
   - License, v. 2.0. If a copy of the MPL was not distributed with this
   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->


<!DOCTYPE bindings [
<!ENTITY % treeDTD SYSTEM "chrome://global/locale/tree.dtd">
%treeDTD;
]>

<bindings id="treeBindings"
   xmlns="http://www.mozilla.org/xbl"
   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
   xmlns:xbl="http://www.mozilla.org/xbl">

  <binding id="tree-base" extends="chrome://global/content/bindings/general.xml#basecontrol">
    <resources>
      <stylesheet src="chrome://global/skin/tree.css"/>
    </resources>
    <implementation>
      <method name="_isAccelPressed">
        <parameter name="aEvent"/>
        <body><![CDATA[
          return aEvent.getModifierState("Accel");
        ]]></body>
      </method>
    </implementation>
  </binding>

  <binding id="tree" extends="chrome://global/content/bindings/tree.xml#tree-base" role="xul:tree">
    <content hidevscroll="true" hidehscroll="true" clickthrough="never">
      <children includes="treecols"/>
      <xul:stack class="tree-stack" flex="1">
        <xul:treerows class="tree-rows" flex="1" xbl:inherits="hidevscroll">
          <children/>
        </xul:treerows>
        <xul:textbox anonid="input" class="tree-input" left="0" top="0" hidden="true"/>
      </xul:stack>
      <xul:hbox xbl:inherits="collapsed=hidehscroll">
        <xul:scrollbar orient="horizontal" flex="1" increment="16" style="position:relative; z-index:2147483647;"/>
        <xul:scrollcorner xbl:inherits="collapsed=hidevscroll"/>
      </xul:hbox>
    </content>

    <implementation implements="nsIDOMXULTreeElement, nsIDOMXULMultiSelectControlElement">

      <!-- ///////////////// nsIDOMXULTreeElement ///////////////// -->

      <property name="columns"
                onget="return this.treeBoxObject.columns;"/>

      <property name="view"
                onget="return this.treeBoxObject.view ?
                       this.treeBoxObject.view.QueryInterface(Components.interfaces.nsITreeView) :
                       null;"
                onset="return this.treeBoxObject.view = val;"/>

      <property name="body"
                onget="return this.treeBoxObject.treeBody;"/>

      <property name="editable"
                onget="return this.getAttribute('editable') == 'true';"
                onset="if (val) this.setAttribute('editable', 'true');
                       else this.removeAttribute('editable'); return val;"/>

      <!-- ///////////////// nsIDOMXULSelectControlElement ///////////////// -->

      <!-- ///////////////// nsIDOMXULMultiSelectControlElement ///////////////// -->

      <property name="selType"
                onget="return this.getAttribute('seltype')"
                onset="this.setAttribute('seltype', val); return val;"/>

      <property name="currentIndex"
                onget="return this.view ? this.view.selection.currentIndex: - 1;"
                onset="if (this.view) return this.view.selection.currentIndex = val; return val;"/>

      <property name="treeBoxObject"
                onget="return this.boxObject;"
                readonly="true"/>
<!-- contentView is obsolete (see bug 202391) -->
      <property name="contentView"
                onget="return this.view; /*.QueryInterface(Components.interfaces.nsITreeContentView)*/"
                readonly="true"/>
<!-- builderView is obsolete (see bug 202393) -->
      <property name="builderView"
                onget="return this.view; /*.QueryInterface(Components.interfaces.nsIXULTreeBuilder)*/"
                readonly="true"/>
      <field name="pageUpOrDownMovesSelection">
        !/Mac/.test(navigator.platform)
      </field>
      <property name="keepCurrentInView"
                onget="return (this.getAttribute('keepcurrentinview') == 'true');"
                onset="if (val) this.setAttribute('keepcurrentinview', 'true');
                       else this.removeAttribute('keepcurrentinview'); return val;"/>

      <property name="enableColumnDrag"
                onget="return this.hasAttribute('enableColumnDrag');"
                onset="if (val) this.setAttribute('enableColumnDrag', 'true');
                       else this.removeAttribute('enableColumnDrag'); return val;"/>

      <field name="_inputField">null</field>

      <property name="inputField" readonly="true">
        <getter><![CDATA[
          if (!this._inputField)
            this._inputField = document.getAnonymousElementByAttribute(this, "anonid", "input");
          return this._inputField;
        ]]></getter>
      </property>

      <property name="disableKeyNavigation"
                onget="return this.hasAttribute('disableKeyNavigation');"
                onset="if (val) this.setAttribute('disableKeyNavigation', 'true');
                       else this.removeAttribute('disableKeyNavigation'); return val;"/>

      <field name="_editingRow">-1</field>
      <field name="_editingColumn">null</field>

      <property name="editingRow" readonly="true"
                onget="return this._editingRow;"/>
      <property name="editingColumn" readonly="true"
                onget="return this._editingColumn;"/>

      <property name="_selectDelay"
                onset="this.setAttribute('_selectDelay', val);"
                onget="return this.getAttribute('_selectDelay') || 50;"/>
      <field name="_columnsDirty">true</field>
      <field name="_lastKeyTime">0</field>
      <field name="_incrementalString">""</field>

      <field name="_touchY">-1</field>

      <method name="_ensureColumnOrder">
        <body><![CDATA[
          if (!this._columnsDirty)
            return;

          if (this.columns) {
            // update the ordinal position of each column to assure that it is
            // an odd number and 2 positions above its next sibling
            var cols = [];
            var i;
            for (var col = this.columns.getFirstColumn(); col; col = col.getNext())
              cols.push(col.element);
            for (i = 0; i < cols.length; ++i)
              cols[i].setAttribute("ordinal", (i*2)+1);

            // update the ordinal positions of splitters to even numbers, so that
            // they are in between columns
            var splitters = this.getElementsByTagName("splitter");
            for (i = 0; i < splitters.length; ++i)
              splitters[i].setAttribute("ordinal", (i+1)*2);
          }
          this._columnsDirty = false;
        ]]></body>
      </method>

      <method name="_reorderColumn">
        <parameter name="aColMove"/>
        <parameter name="aColBefore"/>
        <parameter name="aBefore"/>
        <body><![CDATA[
          this._ensureColumnOrder();

          var i;
          var cols = [];
          var col = this.columns.getColumnFor(aColBefore);
          if (parseInt(aColBefore.ordinal) < parseInt(aColMove.ordinal)) {
            if (aBefore)
              cols.push(aColBefore);
            for (col = col.getNext(); col.element != aColMove;
                 col = col.getNext())
              cols.push(col.element);

            aColMove.ordinal = cols[0].ordinal;
            for (i = 0; i < cols.length; ++i)
              cols[i].ordinal = parseInt(cols[i].ordinal) + 2;
          } else if (aColBefore.ordinal != aColMove.ordinal) {
            if (!aBefore)
              cols.push(aColBefore);
            for (col = col.getPrevious(); col.element != aColMove;
                 col = col.getPrevious())
              cols.push(col.element);

            aColMove.ordinal = cols[0].ordinal;
            for (i = 0; i < cols.length; ++i)
              cols[i].ordinal = parseInt(cols[i].ordinal) - 2;
          }
        ]]></body>
      </method>

      <method name="_getColumnAtX">
        <parameter name="aX"/>
        <parameter name="aThresh"/>
        <parameter name="aPos"/>
        <body><![CDATA[
          var isRTL = document.defaultView.getComputedStyle(this, "")
                              .direction == "rtl";

          if (aPos)
            aPos.value = isRTL ? "after" : "before";

          var columns = [];
          var col = this.columns.getFirstColumn();
          while (col) {
            columns.push(col);
            col = col.getNext();
          }
          if (isRTL)
            columns.reverse();
          var currentX = this.boxObject.x;
          var adjustedX = aX + this.treeBoxObject.horizontalPosition;
          for (var i = 0; i < columns.length; ++i) {
            col = columns[i];
            var cw = col.element.boxObject.width;
            if (cw > 0) {
              currentX += cw;
              if (currentX - (cw * aThresh) > adjustedX)
                return col.element;
            }
          }

          if (aPos)
            aPos.value = isRTL ? "before" : "after";
          return columns.pop().element;
        ]]></body>
      </method>

      <method name="changeOpenState">
        <parameter name="row"/>
        <!-- Optional parameter openState == true or false to set.
             No openState param == toggle -->
        <parameter name="openState"/>
        <body><![CDATA[
          if (row < 0 || !this.view.isContainer(row)) {
            return false;
          }

          if (this.view.isContainerOpen(row) != openState) {
            this.view.toggleOpenState(row);
            if (row == this.currentIndex) {
              // Only fire event when current row is expanded or collapsed
              // because that's all the assistive technology really cares about.
              var event = document.createEvent('Events');
              event.initEvent('OpenStateChange', true, true);
              this.dispatchEvent(event);
            }
            return true;
          }
          return false;
        ]]></body>
      </method>

      <property name="_cellSelType">
        <getter>
          <![CDATA[
            var seltype = this.selType;
            if (seltype == "cell" || seltype == "text")
              return seltype;
            return null;
          ]]>
        </getter>
      </property>

      <method name="_getNextColumn">
        <parameter name="row"/>
        <parameter name="left"/>
        <body><![CDATA[
          var col = this.view.selection.currentColumn;
          if (col) {
            col = left ? col.getPrevious() : col.getNext();
          }
          else {
            col = this.columns.getKeyColumn();
          }
          while (col && (col.width == 0 || !col.selectable ||
                 !this.view.isSelectable(row, col)))
            col = left ? col.getPrevious() : col.getNext();
          return col;
        ]]></body>
      </method>

      <method name="_keyNavigate">
        <parameter name="event"/>
        <body><![CDATA[
          var key = String.fromCharCode(event.charCode).toLowerCase();
          if (event.timeStamp - this._lastKeyTime > 1000)
            this._incrementalString = key;
          else
            this._incrementalString += key;
          this._lastKeyTime = event.timeStamp;

          var length = this._incrementalString.length;
          var incrementalString = this._incrementalString;
          var charIndex = 1;
          while (charIndex < length && incrementalString[charIndex] == incrementalString[charIndex - 1])
            charIndex++;
          // If all letters in incremental string are same, just try to match the first one
          if (charIndex == length) {
            length = 1;
            incrementalString = incrementalString.substring(0, length);
          }

          var keyCol = this.columns.getKeyColumn();
          var rowCount = this.view.rowCount;
          var start = 1;

          var c = this.currentIndex;
          if (length > 1) {
            start = 0;
            if (c < 0)
              c = 0;
          }

          for (var i = 0; i < rowCount; i++) {
            var l = (i + start + c) % rowCount;
            var cellText = this.view.getCellText(l, keyCol);
            cellText = cellText.substring(0, length).toLowerCase();
            if (cellText == incrementalString)
              return l;
          }
          return -1;
        ]]></body>
      </method>

      <method name="startEditing">
        <parameter name="row"/>
        <parameter name="column"/>
        <body>
          <![CDATA[
            if (!this.editable)
              return false;
            if (row < 0 || row >= this.view.rowCount || !column)
              return false;
            if (column.type != Components.interfaces.nsITreeColumn.TYPE_TEXT &&
                column.type != Components.interfaces.nsITreeColumn.TYPE_PASSWORD)
              return false;
            if (column.cycler || !this.view.isEditable(row, column))
              return false;

            // Beyond this point, we are going to edit the cell.
            if (this._editingColumn)
              this.stopEditing();

            var input = this.inputField;

            var box = this.treeBoxObject;
            box.ensureCellIsVisible(row, column);

            // Get the coordinates of the text inside the cell.
            var textRect = box.getCoordsForCellItem(row, column, "text");

            // Get the coordinates of the cell itself.
            var cellRect = box.getCoordsForCellItem(row, column, "cell");

            // Calculate the top offset of the textbox.
            var style = window.getComputedStyle(input, "");
            var topadj = parseInt(style.borderTopWidth) + parseInt(style.paddingTop);
            input.top = textRect.y - topadj;

            // The leftside of the textbox is aligned to the left side of the text
            // in LTR mode, and left side of the cell in RTL mode.
            var left, widthdiff;
            if (style.direction == "rtl") {
              left = cellRect.x;
              widthdiff = cellRect.x - textRect.x;
            } else {
              left = textRect.x;
              widthdiff = textRect.x - cellRect.x;
            }

            input.left = left;
            input.height = textRect.height + topadj +
                           parseInt(style.borderBottomWidth) +
                           parseInt(style.paddingBottom);
            input.width = cellRect.width - widthdiff;
            input.hidden = false;

            input.value = this.view.getCellText(row, column);
            var selectText = function selectText() {
              input.select();
              input.inputField.focus();
            }
            setTimeout(selectText, 0);

            this._editingRow = row;
            this._editingColumn = column;
            this.setAttribute("editing", "true");

            box.invalidateCell(row, column);
            return true;
          ]]>
        </body>
      </method>

      <method name="stopEditing">
        <parameter name="accept"/>
        <body>
          <![CDATA[
            if (!this._editingColumn)
              return;

            var input = this.inputField;
            var editingRow = this._editingRow;
            var editingColumn = this._editingColumn;
            this._editingRow = -1;
            this._editingColumn = null;

            if (accept) {
              var value = input.value;
              this.view.setCellText(editingRow, editingColumn, value);
            }
            input.hidden = true;
            input.value = "";
            this.removeAttribute("editing");
          ]]>
        </body>
      </method>

      <method name="_moveByOffset">
        <parameter name="offset"/>
        <parameter name="edge"/>
        <parameter name="event"/>
        <body>
          <![CDATA[
            event.preventDefault();

            if (this.view.rowCount == 0)
              return;

            if (this._isAccelPressed(event) && this.view.selection.single) {
              this.treeBoxObject.scrollByLines(offset);
              return;
            }

            var c = this.currentIndex + offset;
            if (offset > 0 ? c > edge : c < edge) {
              if (this.view.selection.isSelected(edge) && this.view.selection.count <= 1)
                return;
              c = edge;
            }

            var cellSelType = this._cellSelType;
            if (cellSelType) {
              var column = this.view.selection.currentColumn;
              if (!column)
                return;

              while ((offset > 0 ? c <= edge : c >= edge) && !this.view.isSelectable(c, column))
                c += offset;
              if (offset > 0 ? c > edge : c < edge)
                return;
            }

            if (!this._isAccelPressed(event))
              this.view.selection.timedSelect(c, this._selectDelay);
            else // Ctrl+Up/Down moves the anchor without selecting
              this.currentIndex = c;
            this.treeBoxObject.ensureRowIsVisible(c);
          ]]>
        </body>
      </method>

      <method name="_moveByOffsetShift">
        <parameter name="offset"/>
        <parameter name="edge"/>
        <parameter name="event"/>
        <body>
          <![CDATA[
            event.preventDefault();

            if (this.view.rowCount == 0)
              return;

            if (this.view.selection.single) {
              this.treeBoxObject.scrollByLines(offset);
              return;
            }

            if (this.view.rowCount == 1 && !this.view.selection.isSelected(0)) {
              this.view.selection.timedSelect(0, this._selectDelay);
              return;
            }

            var c = this.currentIndex;
            if (c == -1)
                c = 0;

            if (c == edge) {
              if (this.view.selection.isSelected(c))
                return;
            }

            // Extend the selection from the existing pivot, if any
            this.view.selection.rangedSelect(-1, c + offset,
                                             this._isAccelPressed(event));
            this.treeBoxObject.ensureRowIsVisible(c + offset);

          ]]>
        </body>
      </method>

      <method name="_moveByPage">
        <parameter name="offset"/>
        <parameter name="edge"/>
        <parameter name="event"/>
        <body>
          <![CDATA[
            event.preventDefault();

            if (this.view.rowCount == 0)
              return;

            if (this.pageUpOrDownMovesSelection == this._isAccelPressed(event)) {
               this.treeBoxObject.scrollByPages(offset);
               return;
            }

            if (this.view.rowCount == 1 && !this.view.selection.isSelected(0)) {
              this.view.selection.timedSelect(0, this._selectDelay);
              return;
            }

            var c = this.currentIndex;
            if (c == -1)
              return;

            if (c == edge && this.view.selection.isSelected(c)) {
              this.treeBoxObject.ensureRowIsVisible(c);
              return;
            }
            var i = this.treeBoxObject.getFirstVisibleRow();
            var p = this.treeBoxObject.getPageLength();

            if (offset > 0) {
              i += p - 1;
              if (c >= i) {
                 i = c + p;
                 this.treeBoxObject.ensureRowIsVisible(i > edge ? edge : i);
              }
              i = i > edge ? edge : i;

            } else if (c <= i) {
               i = c <= p ? 0 : c - p;
               this.treeBoxObject.ensureRowIsVisible(i);
            }
            this.view.selection.timedSelect(i, this._selectDelay);
          ]]>
        </body>
      </method>

      <method name="_moveByPageShift">
        <parameter name="offset"/>
        <parameter name="edge"/>
        <parameter name="event"/>
        <body>
          <![CDATA[
            event.preventDefault();

            if (this.view.rowCount == 0)
              return;

            if (this.view.rowCount == 1 && !this.view.selection.isSelected(0) &&
                !(this.pageUpOrDownMovesSelection == this._isAccelPressed(event))) {
              this.view.selection.timedSelect(0, this._selectDelay);
              return;
            }

            if (this.view.selection.single)
              return;

            var c = this.currentIndex;
            if (c == -1)
              return;
            if (c == edge && this.view.selection.isSelected(c)) {
              this.treeBoxObject.ensureRowIsVisible(edge);
              return;
            }
            var i = this.treeBoxObject.getFirstVisibleRow();
            var p = this.treeBoxObject.getPageLength();

            if (offset > 0) {
              i += p - 1;
              if (c >= i) {
                 i = c + p;
                 this.treeBoxObject.ensureRowIsVisible(i > edge ? edge : i);
              }
              // Extend the selection from the existing pivot, if any
              this.view.selection.rangedSelect(-1, i > edge ? edge : i, this._isAccelPressed(event));

            } else {

              if (c <= i) {
                 i = c <= p ? 0 : c - p;
                 this.treeBoxObject.ensureRowIsVisible(i);
              }
              // Extend the selection from the existing pivot, if any
              this.view.selection.rangedSelect(-1, i, this._isAccelPressed(event));
            }

          ]]>
        </body>
      </method>

      <method name="_moveToEdge">
        <parameter name="edge"/>
        <parameter name="event"/>
        <body>
          <![CDATA[
            event.preventDefault();

            if (this.view.rowCount == 0)
              return;

            if (this.view.selection.isSelected(edge) && this.view.selection.count == 1) {
              this.currentIndex = edge;
              return;
            }

            // Normal behaviour is to select the first/last row
            if (!this._isAccelPressed(event))
              this.view.selection.timedSelect(edge, this._selectDelay);

            // In a multiselect tree Ctrl+Home/End moves the anchor
            else if (!this.view.selection.single)
              this.currentIndex = edge;

            this.treeBoxObject.ensureRowIsVisible(edge);
          ]]>
        </body>
      </method>

      <method name="_moveToEdgeShift">
        <parameter name="edge"/>
        <parameter name="event"/>
        <body>
          <![CDATA[
            event.preventDefault();

            if (this.view.rowCount == 0)
              return;

            if (this.view.rowCount == 1 && !this.view.selection.isSelected(0)) {
              this.view.selection.timedSelect(0, this._selectDelay);
              return;
            }

            if (this.view.selection.single ||
                (this.view.selection.isSelected(edge)) && this.view.selection.isSelected(this.currentIndex))
              return;

            // Extend the selection from the existing pivot, if any.
            // -1 doesn't work here, so using currentIndex instead
            this.view.selection.rangedSelect(this.currentIndex, edge, this._isAccelPressed(event));

            this.treeBoxObject.ensureRowIsVisible(edge);
          ]]>
        </body>
      </method>
      <method name="_handleEnter">
        <parameter name="event"/>
        <body><![CDATA[
          if (this._editingColumn) {
            this.stopEditing(true);
            this.focus();
            return true;
          }

          if (/Mac/.test(navigator.platform)) {
            // See if we can edit the cell.
            var row = this.currentIndex;
            if (this._cellSelType) {
              var column = this.view.selection.currentColumn;
              var startedEditing = this.startEditing(row, column);
              if (startedEditing)
                return true;
            }
          }
          return this.changeOpenState(this.currentIndex);
        ]]></body>
      </method>
    </implementation>

    <handlers>
      <handler event="touchstart">
        <![CDATA[
          if (event.touches.length > 1) {
            // Multiple touch points detected, abort. In particular this aborts
            // the panning gesture when the user puts a second finger down after
            // already panning with one finger. Aborting at this point prevents
            // the pan gesture from being resumed until all fingers are lifted
            // (as opposed to when the user is back down to one finger).
            this._touchY = -1;
          } else {
            this._touchY = event.touches[0].screenY;
          }
        ]]>
      </handler>
      <handler event="touchmove">
        <![CDATA[
          if (event.touches.length == 1 &&
              this._touchY >= 0) {
            var deltaY = this._touchY - event.touches[0].screenY;
            var lines = Math.trunc(deltaY / this.treeBoxObject.rowHeight);
            if (Math.abs(lines) > 0) {
              this.treeBoxObject.scrollByLines(lines);
              deltaY -= lines * this.treeBoxObject.rowHeight;
              this._touchY = event.touches[0].screenY + deltaY;
            }
            event.preventDefault();
          }
        ]]>
      </handler>
      <handler event="touchend">
        <![CDATA[
          this._touchY = -1;
        ]]>
      </handler>
      <handler event="MozMousePixelScroll" preventdefault="true"/>
      <handler event="DOMMouseScroll" preventdefault="true">
        <![CDATA[
          if (this._editingColumn)
            return;
          if (event.axis == event.HORIZONTAL_AXIS)
            return;

          var rows = event.detail;
          if (rows == UIEvent.SCROLL_PAGE_UP)
            this.treeBoxObject.scrollByPages(-1);
          else if (rows == UIEvent.SCROLL_PAGE_DOWN)
            this.treeBoxObject.scrollByPages(1);
          else
            this.treeBoxObject.scrollByLines(rows);
        ]]>
      </handler>
      <handler event="MozSwipeGesture" preventdefault="true">
        <![CDATA[
          // Figure out which row to show
          let targetRow = 0;

          // Only handle swipe gestures up and down
          switch (event.direction) {
            case event.DIRECTION_DOWN:
              targetRow = this.view.rowCount - 1;
              // Fall through for actual action
            case event.DIRECTION_UP:
              this.treeBoxObject.ensureRowIsVisible(targetRow);
              break;
          }
        ]]>
      </handler>
      <handler event="select" phase="target"
               action="if (event.originalTarget == this) this.stopEditing(true);"/>
      <handler event="focus">
        <![CDATA[
          this.treeBoxObject.focused = true;
          if (this.currentIndex == -1 && this.view.rowCount > 0) {
            this.currentIndex = this.treeBoxObject.getFirstVisibleRow();
          }
          if (this._cellSelType && !this.view.selection.currentColumn) {
            var col = this._getNextColumn(this.currentIndex, false);
            this.view.selection.currentColumn = col;
          }
        ]]>
      </handler>
      <handler event="blur" action="this.treeBoxObject.focused = false;"/>
      <handler event="blur" phase="capturing"
               action="if (event.originalTarget == this.inputField.inputField) this.stopEditing(true);"/>
      <handler event="keydown" keycode="VK_RETURN">
        if (this._handleEnter(event)) {
          event.stopPropagation();
          event.preventDefault();
        }
      </handler>
      <!-- Use F2 key to enter text editing. -->
      <handler event="keydown" keycode="VK_F2">
        <![CDATA[
          if (!this._cellSelType)
            return;
          var row = this.currentIndex;
          var column = this.view.selection.currentColumn;
          if (this.startEditing(row, column))
            event.preventDefault();
        ]]>
      </handler>

      <handler event="keydown" keycode="VK_ESCAPE">
        <![CDATA[
          if (this._editingColumn) {
            this.stopEditing(false);
            this.focus();
            event.stopPropagation();
            event.preventDefault();
          }
        ]]>
      </handler>
      <handler event="keydown" keycode="VK_LEFT">
        <![CDATA[
         if (this._editingColumn)
           return;

         var row = this.currentIndex;
         if (row < 0)
           return;

         var cellSelType = this._cellSelType;
         var checkContainers = true;

         var currentColumn;
         if (cellSelType) {
           currentColumn = this.view.selection.currentColumn;
           if (currentColumn && !currentColumn.primary)
             checkContainers = false;
         }

         if (checkContainers) {
           if (this.changeOpenState(this.currentIndex, false)) {
             event.preventDefault();
             return;
           }
           var parentIndex = this.view.getParentIndex(this.currentIndex);
           if (parentIndex >= 0) {
             if (cellSelType && !this.view.isSelectable(parentIndex, currentColumn)) {
               return;
             }
             this.view.selection.select(parentIndex);
             this.treeBoxObject.ensureRowIsVisible(parentIndex);
             event.preventDefault();
             return;
           }
         }

         if (cellSelType) {
           var col = this._getNextColumn(row, true);
           if (col) {
             this.view.selection.currentColumn = col;
             this.treeBoxObject.ensureCellIsVisible(row, col);
             event.preventDefault();
           }
         }
        ]]>
      </handler>
      <handler event="keydown" keycode="VK_RIGHT">
        <![CDATA[
         if (this._editingColumn)
           return;

          var row = this.currentIndex;
          if (row < 0)
            return;

          var cellSelType = this._cellSelType;
          var checkContainers = true;

          var currentColumn;
          if (cellSelType) {
            currentColumn = this.view.selection.currentColumn;
            if (currentColumn && !currentColumn.primary)
              checkContainers = false;
          }

          if (checkContainers) {
            if (this.changeOpenState(row, true)) {
              event.preventDefault();
              return;
            }
            var c = row + 1;
            var view = this.view;
            if (c < view.rowCount &&
                view.getParentIndex(c) == row) {
              // If already opened, select the first child.
              // The getParentIndex test above ensures that the children
              // are already populated and ready.
              if (cellSelType && !this.view.isSelectable(c, currentColumn)) {
                let col = this._getNextColumn(c, false);
                if (col) {
                  this.view.selection.currentColumn = col;
                }
              }
              this.view.selection.timedSelect(c, this._selectDelay);
              this.treeBoxObject.ensureRowIsVisible(c);
              event.preventDefault();
              return;
            }
          }

          if (cellSelType) {
            let col = this._getNextColumn(row, false);
            if (col) {
              this.view.selection.currentColumn = col;
              this.treeBoxObject.ensureCellIsVisible(row, col);
              event.preventDefault();
            }
          }
        ]]>
      </handler>
      <handler event="keydown" keycode="VK_UP" modifiers="accel any">
        <![CDATA[
          if (this._editingColumn)
            return;
          _moveByOffset(-1, 0, event);
        ]]>
      </handler>
      <handler event="keydown" keycode="VK_DOWN" modifiers="accel any">
        <![CDATA[
          if (this._editingColumn)
            return;
          _moveByOffset(1, this.view.rowCount - 1, event);
        ]]>
      </handler>
      <handler event="keydown" keycode="VK_UP" modifiers="accel any, shift">
        <![CDATA[
          if (this._editingColumn)
            return;
          _moveByOffsetShift(-1, 0, event);
        ]]>
      </handler>
      <handler event="keydown" keycode="VK_DOWN" modifiers="accel any, shift">
        <![CDATA[
          if (this._editingColumn)
            return;
          _moveByOffsetShift(1, this.view.rowCount - 1, event);
        ]]>
      </handler>
      <handler event="keydown" keycode="VK_PAGE_UP" modifiers="accel any">
        <![CDATA[
          if (this._editingColumn)
            return;
          _moveByPage(-1, 0, event);
        ]]>
      </handler>
      <handler event="keydown" keycode="VK_PAGE_DOWN" modifiers="accel any">
        <![CDATA[
          if (this._editingColumn)
            return;
          _moveByPage(1, this.view.rowCount - 1, event);
        ]]>
      </handler>
      <handler event="keydown" keycode="VK_PAGE_UP" modifiers="accel any, shift">
        <![CDATA[
          if (this._editingColumn)
            return;
          _moveByPageShift(-1, 0, event);
        ]]>
      </handler>
      <handler event="keydown" keycode="VK_PAGE_DOWN" modifiers="accel any, shift">
        <![CDATA[
          if (this._editingColumn)
            return;
          _moveByPageShift(1, this.view.rowCount - 1, event);
        ]]>
      </handler>
      <handler event="keydown" keycode="VK_HOME" modifiers="accel any">
        <![CDATA[
          if (this._editingColumn)
            return;
          _moveToEdge(0, event);
        ]]>
      </handler>
      <handler event="keydown" keycode="VK_END" modifiers="accel any">
        <![CDATA[
          if (this._editingColumn)
            return;
          _moveToEdge(this.view.rowCount - 1, event);
        ]]>
      </handler>
      <handler event="keydown" keycode="VK_HOME" modifiers="accel any, shift">
        <![CDATA[
          if (this._editingColumn)
            return;
          _moveToEdgeShift(0, event);
        ]]>
      </handler>
      <handler event="keydown" keycode="VK_END" modifiers="accel any, shift">
        <![CDATA[
          if (this._editingColumn)
            return;
          _moveToEdgeShift(this.view.rowCount - 1, event);
        ]]>
      </handler>
      <handler event="keypress">
        <![CDATA[
         if (this._editingColumn)
           return;

         if (event.charCode == ' '.charCodeAt(0)) {
           var c = this.currentIndex;
           if (!this.view.selection.isSelected(c) ||
               (!this.view.selection.single && this._isAccelPressed(event))) {
             this.view.selection.toggleSelect(c);
             event.preventDefault();
           }
         }
         else if (!this.disableKeyNavigation && event.charCode > 0 &&
                  !event.altKey && !this._isAccelPressed(event) &&
                  !event.metaKey && !event.ctrlKey) {
           var l = this._keyNavigate(event);
           if (l >= 0) {
             this.view.selection.timedSelect(l, this._selectDelay);
             this.treeBoxObject.ensureRowIsVisible(l);
           }
           event.preventDefault();
         }
         ]]>
      </handler>
    </handlers>
  </binding>

  <binding id="treecols" role="xul:treecolumns">
    <resources>
      <stylesheet src="chrome://global/skin/tree.css"/>
    </resources>
    <content orient="horizontal">
      <xul:hbox class="tree-scrollable-columns" flex="1">
        <children includes="treecol|splitter"/>
      </xul:hbox>
      <xul:treecolpicker class="treecol-image" fixed="true" xbl:inherits="tooltiptext=pickertooltiptext"/>
    </content>
    <implementation>
      <constructor><![CDATA[
        // Set resizeafter="farthest" on the splitters if nothing else has been
        // specified.
        Array.forEach(this.getElementsByTagName("splitter"), function (splitter) {
          if (!splitter.hasAttribute("resizeafter"))
            splitter.setAttribute("resizeafter", "farthest");
        });
      ]]></constructor>
    </implementation>
  </binding>

  <binding id="treerows" extends="chrome://global/content/bindings/tree.xml#tree-base">
    <content>
      <xul:hbox flex="1" class="tree-bodybox">
        <children/>
      </xul:hbox>
      <xul:scrollbar height="0" minwidth="0" minheight="0" orient="vertical" xbl:inherits="collapsed=hidevscroll" style="position:relative; z-index:2147483647;"/>
    </content>
    <handlers>
      <handler event="underflow">
        <![CDATA[
          // Scrollport event orientation
          // 0: vertical
          // 1: horizontal
          // 2: both (not used)
          var tree = document.getBindingParent(this);
          if (event.detail == 1)
            tree.setAttribute("hidehscroll", "true");
          else if (event.detail == 0)
            tree.setAttribute("hidevscroll", "true");
          event.stopPropagation();
        ]]>
      </handler>
      <handler event="overflow">
        <![CDATA[
          var tree = document.getBindingParent(this);
          if (event.detail == 1)
            tree.removeAttribute("hidehscroll");
          else if (event.detail == 0)
            tree.removeAttribute("hidevscroll");
          event.stopPropagation();
        ]]>
      </handler>
    </handlers>
  </binding>

  <binding id="treebody" extends="chrome://global/content/bindings/tree.xml#tree-base">
    <implementation>
      <constructor>
        if ("_ensureColumnOrder" in this.parentNode)
          this.parentNode._ensureColumnOrder();
      </constructor>

      <field name="_lastSelectedRow">
        -1
      </field>
    </implementation>
    <handlers>
      <!-- If there is no modifier key, we select on mousedown, not
           click, so that drags work correctly. -->
      <handler event="mousedown" clickcount="1">
      <![CDATA[
         if (this.parentNode.disabled)
           return;
         if (((!this._isAccelPressed(event) ||
             !this.parentNode.pageUpOrDownMovesSelection) &&
             !event.shiftKey && !event.metaKey) ||
             this.parentNode.view.selection.single) {
           var b = this.parentNode.treeBoxObject;
           var cell = b.getCellAt(event.clientX, event.clientY);
           var view = this.parentNode.view;

           // save off the last selected row
           this._lastSelectedRow = cell.row;

           if (cell.row == -1)
             return;

           if (cell.childElt == "twisty")
             return;

           if (cell.col && event.button == 0) {
             if (cell.col.cycler) {
               view.cycleCell(cell.row, cell.col);
               return;
             } else if (cell.col.type == Components.interfaces.nsITreeColumn.TYPE_CHECKBOX) {
               if (this.parentNode.editable && cell.col.editable &&
                   view.isEditable(cell.row, cell.col)) {
                 var value = view.getCellValue(cell.row, cell.col);
                 value = value == "true" ? "false" : "true";
                 view.setCellValue(cell.row, cell.col, value);
                 return;
               }
             }
           }

           var cellSelType = this.parentNode._cellSelType;
           if (cellSelType == "text" && cell.childElt != "text" && cell.childElt != "image")
             return;

           if (cellSelType) {
             if (!cell.col.selectable ||
                 !view.isSelectable(cell.row, cell.col)) {
               return;
             }
           }

           if (!view.selection.isSelected(cell.row)) {
             view.selection.select(cell.row);
             b.ensureRowIsVisible(cell.row);
           }

           if (cellSelType) {
             view.selection.currentColumn = cell.col;
           }
         }
      ]]>
      </handler>

      <!-- On a click (up+down on the same item), deselect everything
           except this item. -->
      <handler event="click" button="0" clickcount="1">
      <![CDATA[
        if (this.parentNode.disabled)
          return;
        var b = this.parentNode.treeBoxObject;
        var cell = b.getCellAt(event.clientX, event.clientY);
        var view = this.parentNode.view;

        if (cell.row == -1)
          return;

        if (cell.childElt == "twisty") {
          if (view.selection.currentIndex >= 0 &&
              view.isContainerOpen(cell.row)) {
            var parentIndex = view.getParentIndex(view.selection.currentIndex);
            while (parentIndex >= 0 && parentIndex != cell.row)
              parentIndex = view.getParentIndex(parentIndex);
            if (parentIndex == cell.row) {
              var parentSelectable = true;
              if (this.parentNode._cellSelType) {
                var currentColumn = view.selection.currentColumn;
                if (!view.isSelectable(parentIndex, currentColumn))
                  parentSelectable = false;
              }
              if (parentSelectable)
                view.selection.select(parentIndex);
            }
          }
          this.parentNode.changeOpenState(cell.row);
          return;
        }

        if (! view.selection.single) {
          var augment = this._isAccelPressed(event);
          if (event.shiftKey) {
            view.selection.rangedSelect(-1, cell.row, augment);
            b.ensureRowIsVisible(cell.row);
            return;
          }
          if (augment) {
            view.selection.toggleSelect(cell.row);
            b.ensureRowIsVisible(cell.row);
            view.selection.currentIndex = cell.row;
            return;
          }
        }

        /* We want to deselect all the selected items except what was
          clicked, UNLESS it was a right-click.  We have to do this
          in click rather than mousedown so that you can drag a
          selected group of items */

        if (!cell.col) return;

        // if the last row has changed in between the time we
        // mousedown and the time we click, don't fire the select handler.
        // see bug #92366
        if (!cell.col.cycler && this._lastSelectedRow == cell.row &&
            cell.col.type != Components.interfaces.nsITreeColumn.TYPE_CHECKBOX) {

          var cellSelType = this.parentNode._cellSelType;
          if (cellSelType == "text" && cell.childElt != "text" && cell.childElt != "image")
            return;

          if (cellSelType) {
            if (!cell.col.selectable ||
                !view.isSelectable(cell.row, cell.col)) {
              return;
            }
          }

          view.selection.select(cell.row);
          b.ensureRowIsVisible(cell.row);

          if (cellSelType) {
            view.selection.currentColumn = cell.col;
          }
        }
      ]]>
      </handler>

      <!-- double-click -->
      <handler event="click" clickcount="2">
      <![CDATA[
        if (this.parentNode.disabled)
          return;
        var tbo = this.parentNode.treeBoxObject;
        var view = this.parentNode.view;
        var row = view.selection.currentIndex;

        if (row == -1)
          return;

        var cell = tbo.getCellAt(event.clientX, event.clientY);

        if (cell.childElt != "twisty") {
          view.selection.currentColumn = cell.col;
          this.parentNode.startEditing(row, cell.col);
        }

        if (this.parentNode._editingColumn || !view.isContainer(row))
          return;

        // Cyclers and twisties respond to single clicks, not double clicks
        if (cell.col && !cell.col.cycler && cell.childElt != "twisty")
          this.parentNode.changeOpenState(row);
      ]]>
      </handler>

    </handlers>
  </binding>

  <binding id="treecol-base" role="xul:treecolumnitem"
           extends="chrome://global/content/bindings/tree.xml#tree-base">
    <implementation>
      <constructor>
        this.parentNode.parentNode._columnsDirty = true;
      </constructor>

      <property name="ordinal">
        <getter><![CDATA[
          var val = this.getAttribute("ordinal");
          if (val == "")
            return "1";

          return "" + (val == "0" ? 0 : parseInt(val));
        ]]></getter>
        <setter><![CDATA[
          this.setAttribute("ordinal", val);
          return val;
        ]]></setter>
      </property>

      <property name="_previousVisibleColumn">
        <getter><![CDATA[
          var sib = this.boxObject.previousSibling;
          while (sib) {
            if (sib.localName == "treecol" && sib.boxObject.width > 0 && sib.parentNode == this.parentNode)
              return sib;
            sib = sib.boxObject.previousSibling;
          }
          return null;
        ]]></getter>
      </property>

      <method name="_onDragMouseMove">
        <parameter name="aEvent"/>
        <body><![CDATA[
          var col = document.treecolDragging;
          if (!col) return;

          // determine if we have moved the mouse far enough
          // to initiate a drag
          if (col.mDragGesturing) {
            if (Math.abs(aEvent.clientX - col.mStartDragX) < 5 &&
                Math.abs(aEvent.clientY - col.mStartDragY) < 5) {
              return;
            }
            col.mDragGesturing = false;
            col.setAttribute("dragging", "true");
            window.addEventListener("click", col._onDragMouseClick, true);
          }

          var pos = {};
          var targetCol = col.parentNode.parentNode._getColumnAtX(aEvent.clientX, 0.5, pos);

          // bail if we haven't mousemoved to a different column
          if (col.mTargetCol == targetCol && col.mTargetDir == pos.value)
            return;

          var tree = col.parentNode.parentNode;
          var sib;
          var column;
          if (col.mTargetCol) {
            // remove previous insertbefore/after attributes
            col.mTargetCol.removeAttribute("insertbefore");
            col.mTargetCol.removeAttribute("insertafter");
            column = tree.columns.getColumnFor(col.mTargetCol);
            tree.treeBoxObject.invalidateColumn(column);
            sib = col.mTargetCol._previousVisibleColumn;
            if (sib) {
              sib.removeAttribute("insertafter");
              column = tree.columns.getColumnFor(sib);
              tree.treeBoxObject.invalidateColumn(column);
            }
            col.mTargetCol = null;
            col.mTargetDir = null;
          }

          if (targetCol) {
            // set insertbefore/after attributes
            if (pos.value == "after") {
              targetCol.setAttribute("insertafter", "true");
            } else {
              targetCol.setAttribute("insertbefore", "true");
              sib = targetCol._previousVisibleColumn;
              if (sib) {
                sib.setAttribute("insertafter", "true");
                column = tree.columns.getColumnFor(sib);
                tree.treeBoxObject.invalidateColumn(column);
              }
            }
            column = tree.columns.getColumnFor(targetCol);
            tree.treeBoxObject.invalidateColumn(column);
            col.mTargetCol = targetCol;
            col.mTargetDir = pos.value;
          }
        ]]></body>
      </method>

      <method name="_onDragMouseUp">
        <parameter name="aEvent"/>
        <body><![CDATA[
          var col = document.treecolDragging;
          if (!col) return;

          if (!col.mDragGesturing) {
            if (col.mTargetCol) {
              // remove insertbefore/after attributes
              var before = col.mTargetCol.hasAttribute("insertbefore");
              col.mTargetCol.removeAttribute(before ? "insertbefore" : "insertafter");

              var sib = col.mTargetCol._previousVisibleColumn;
              if (before && sib) {
                sib.removeAttribute("insertafter");
              }

              // Move the column only if it will result in a different column
              // ordering
              var move = true;

              // If this is a before move and the previous visible column is
              // the same as the column we're moving, don't move
              if (before && col == sib) {
                move = false;
              }
              else if (!before && col == col.mTargetCol) {
                // If this is an after move and the column we're moving is
                // the same as the target column, don't move.
                move = false;
              }

              if (move) {
                col.parentNode.parentNode._reorderColumn(col, col.mTargetCol, before);
              }

              // repaint to remove lines
              col.parentNode.parentNode.treeBoxObject.invalidate();

              col.mTargetCol = null;
            }
          } else
            col.mDragGesturing = false;

          document.treecolDragging = null;
          col.removeAttribute("dragging");

          window.removeEventListener("mousemove", col._onDragMouseMove, true);
          window.removeEventListener("mouseup", col._onDragMouseUp, true);
          // we have to wait for the click event to fire before removing
          // cancelling handler
          var clickHandler = function(handler) {
            window.removeEventListener("click", handler, true);
          };
          window.setTimeout(clickHandler, 0, col._onDragMouseClick);
        ]]></body>
      </method>

      <method name="_onDragMouseClick">
        <parameter name="aEvent"/>
        <body><![CDATA[
          // prevent click event from firing after column drag and drop
          aEvent.stopPropagation();
          aEvent.preventDefault();
        ]]></body>
      </method>
    </implementation>

    <handlers>
      <handler event="mousedown" button="0"><![CDATA[
        if (this.parentNode.parentNode.enableColumnDrag) {
          var xulns = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
          var cols = this.parentNode.getElementsByTagNameNS(xulns, "treecol");

          // only start column drag operation if there are at least 2 visible columns
          var visible = 0;
          for (var i = 0; i < cols.length; ++i)
            if (cols[i].boxObject.width > 0) ++visible;

          if (visible > 1) {
            window.addEventListener("mousemove", this._onDragMouseMove, true);
            window.addEventListener("mouseup", this._onDragMouseUp, true);
            document.treecolDragging = this;
            this.mDragGesturing = true;
            this.mStartDragX = event.clientX;
            this.mStartDragY = event.clientY;
          }
        }
      ]]></handler>
      <handler event="click" button="0" phase="target">
        <![CDATA[
          if (event.target != event.originalTarget)
            return;

          // On Windows multiple clicking on tree columns only cycles one time
          // every 2 clicks.
          if (/Win/.test(navigator.platform) && event.detail % 2 == 0)
            return;

          var tree = this.parentNode.parentNode;
          if (tree.columns) {
            tree.view.cycleHeader(tree.columns.getColumnFor(this));
          }
        ]]>
      </handler>
    </handlers>
  </binding>

  <binding id="treecol" extends="chrome://global/content/bindings/tree.xml#treecol-base">
    <content>
      <xul:label class="treecol-text" xbl:inherits="crop,value=label" flex="1" crop="right"/>
      <xul:image class="treecol-sortdirection" xbl:inherits="sortDirection,hidden=hideheader"/>
    </content>
  </binding>

  <binding id="treecol-image" extends="chrome://global/content/bindings/tree.xml#treecol-base">
    <content>
      <xul:image class="treecol-icon" xbl:inherits="src"/>
    </content>
  </binding>

  <binding id="columnpicker" display="xul:button" role="xul:button"
           extends="chrome://global/content/bindings/tree.xml#tree-base">
    <content>
      <xul:image class="tree-columnpicker-icon"/>
      <xul:menupopup anonid="popup">
        <xul:menuseparator anonid="menuseparator"/>
        <xul:menuitem anonid="menuitem" label="&restoreColumnOrder.label;"/>
      </xul:menupopup>
    </content>

    <implementation>
      <method name="buildPopup">
        <parameter name="aPopup"/>
        <body>
          <![CDATA[
            // We no longer cache the picker content, remove the old content.
            while (aPopup.childNodes.length > 2)
              aPopup.removeChild(aPopup.firstChild);

            var refChild = aPopup.firstChild;

            var tree = this.parentNode.parentNode;
            for (var currCol = tree.columns.getFirstColumn(); currCol;
                 currCol = currCol.getNext()) {
              // Construct an entry for each column in the row, unless
              // it is not being shown.
              var currElement = currCol.element;
              if (!currElement.hasAttribute("ignoreincolumnpicker")) {
                var popupChild = document.createElement("menuitem");
                popupChild.setAttribute("type", "checkbox");
                var columnName = currElement.getAttribute("display") ||
                                 currElement.getAttribute("label");
                popupChild.setAttribute("label", columnName);
                popupChild.setAttribute("colindex", currCol.index);
                if (currElement.getAttribute("hidden") != "true")
                  popupChild.setAttribute("checked", "true");
                if (currCol.primary)
                  popupChild.setAttribute("disabled", "true");
                aPopup.insertBefore(popupChild, refChild);
              }
            }

            var hidden = !tree.enableColumnDrag;
            const anonids = ["menuseparator", "menuitem"];
            for (var i = 0; i < anonids.length; i++) {
              var element = document.getAnonymousElementByAttribute(this, "anonid", anonids[i]);
              element.hidden = hidden;
            }
          ]]>
        </body>
      </method>
    </implementation>

    <handlers>
      <handler event="command">
        <![CDATA[
          if (event.originalTarget == this) {
            var popup = document.getAnonymousElementByAttribute(this, "anonid", "popup");
            this.buildPopup(popup);
            popup.showPopup(this, -1, -1, "popup", "bottomright", "topright");
          }
          else {
            var tree = this.parentNode.parentNode;
            tree.stopEditing(true);
            var menuitem = document.getAnonymousElementByAttribute(this, "anonid", "menuitem");
            if (event.originalTarget == menuitem) {
              tree.columns.restoreNaturalOrder();
              tree._ensureColumnOrder();
            }
            else {
              var colindex = event.originalTarget.getAttribute("colindex");
              var column = tree.columns[colindex];
              if (column) {
                var element = column.element;
                if (element.getAttribute("hidden") == "true")
                  element.setAttribute("hidden", "false");
                else
                  element.setAttribute("hidden", "true");
              }
            }
          }
        ]]>
      </handler>
    </handlers>
  </binding>
</bindings>
